package com.anb.net;

import com.google.protobuf.GeneratedMessage;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

public class Packet {
    public Packet(ICmdHelper cmdHelper) {
        this.cmdHelper = cmdHelper;
    }

    public interface ICmdHelper {
        CmdHandler getCmdHandler(short cmd) throws Exception;
        CmdHandler getCmdHandler(String cmd) throws Exception;
    }

    private ICmdHelper cmdHelper;

    public static final int HEAD_LEN = 6;

    public enum Flags {
        BodyTypeBits(0x0f),
        Response(0x10),
        NeedResponse(0x20),
        Compress(0x40);

        private int value;

        Flags(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }

        private static final Map<Integer, Flags> valuesMap = new HashMap<Integer, Flags>();
        static {
            for (Flags code : Flags.values()) {
                valuesMap.put(code.getValue(), code);
            }
        }

        public static Flags from(int value) throws Exception {
            Flags flags = valuesMap.get(value);
            if (flags == null) {
                throw new Exception("cannot convert Packet.BodyType from " + value);
            }
            return flags;
        }
    }

    public enum BodyType {
        None(0),
        Binary(1),
        Boolean(2),
        Integer(3),
        UInteger(4),
        String(5),
        ProtoBuf(6);

        private int value;

        BodyType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }

        private static final Map<Integer, BodyType> valuesMap = new HashMap<Integer, BodyType>();
        static {
            for (BodyType code : BodyType.values()) {
                valuesMap.put(code.getValue(), code);
            }
        }

        public static BodyType from(int value) throws Exception {
            BodyType type = valuesMap.get(value);
            if (type == null) {
                throw new Exception("cannot convert Packet.BodyType from " + value);
            }
            return type;
        }

        public static BodyType from(Class<?> clazz) throws Exception {
            if (clazz == null || clazz == Void.class) {
                return None;
            }
            if (clazz == byte[].class) {
                return Binary;
            }
            if (clazz == boolean.class) {
                return Boolean;
            }
            if (clazz == char.class || clazz == short.class || clazz == int.class || clazz == long.class) {
                return Integer;
            }
            if (clazz == byte.class || clazz == short.class || clazz == int.class || clazz == long.class) {
                return UInteger;
            }
            if (clazz == java.lang.String.class) {
                return String;
            }
            if (com.google.protobuf.GeneratedMessage.class.isAssignableFrom(clazz)) {
                return ProtoBuf;
            }
            throw new Exception("unsupported BodyType for " + clazz.getName());
        }
    }

    public short id;
    public short cmd;
    public BodyType type;
    public boolean response;
    public boolean needResponse;
    public boolean compress;
    public Error.Code error;
    public Object data;

    public void deserializeHead(ByteBuffer buffer) throws Exception {
        buffer.order(ByteOrder.BIG_ENDIAN);

        id = buffer.getShort();
        cmd = buffer.getShort();
        byte flags = buffer.get();
        type = BodyType.from(new Byte(flags).intValue() & Flags.BodyTypeBits.getValue());
        response = (flags & (byte) Flags.Response.getValue()) != 0;
        needResponse = (flags & (byte) Flags.NeedResponse.getValue()) != 0;
        compress = (flags & (byte) Flags.Compress.getValue()) != 0;

        error = Error.Code.from(new Byte(buffer.get()).intValue());
    }

    public void deserializeBody(ByteBuffer buffer) throws Exception {
        buffer.order(ByteOrder.BIG_ENDIAN);

        int bodyLen = buffer.remaining();
        switch (type) {
            case None:
                data = null;
                break;
            case Binary:
                byte[] bytes = new byte[bodyLen];
                buffer.get(bytes);
                data = bytes;
                break;
            case Boolean:
                data = bodyLen > 0 && buffer.getChar() != 0;
                break;
            case Integer:
                switch (bodyLen) {
                    case 8:
                        data = buffer.getLong();
                        break;
                    case 4:
                        data = buffer.getInt();
                        break;
                    case 2:
                        data = buffer.getShort();
                        break;
                    case 1:
                        data = buffer.getChar();
                        break;
                    default:
                        throw new Exception("unknown integer length " + bodyLen);
                }
                break;
            case UInteger:
                switch (bodyLen) {
                    case 8:
                        data = buffer.getLong();
                        break;
                    case 4:
                        data = buffer.getInt();
                        break;
                    case 2:
                        data = buffer.getShort();
                        break;
                    case 1:
                        data = buffer.get();
                        break;
                    default:
                        throw new Exception("unknown unsigned integer length " + bodyLen);
                }
                break;
            case String:
                data = new String(buffer.array(), buffer.position(), buffer.remaining(), Charset.forName("UTF-8"));
                break;
            case ProtoBuf:
                CmdHandler cmdHandler = cmdHelper.getCmdHandler(cmd);
                data = cmdHandler.inParser.parseFrom(buffer.array(), buffer.position(), buffer.remaining());
                break;
            default:
                throw new Exception();
                //break;
        }
    }

    public ByteBuffer[] serialize() throws Exception {
        byte flags = (byte)(type.getValue() & Flags.BodyTypeBits.getValue());
        if (response) {
            flags |= (byte)Flags.Response.getValue();
        }
        if (needResponse) {
            flags |= (byte)Flags.NeedResponse.getValue();
        }

        ByteBuffer head = ByteBuffer.allocate(HEAD_LEN)
                .order(ByteOrder.BIG_ENDIAN)
                .putShort(id)
                .putShort(cmd)
                .put(flags)
                .put((byte)error.getValue());
        head.flip();

        ByteBuffer body = serializeBody();

        return new ByteBuffer[] {
                head,
                body
        };
    }
    private ByteBuffer serializeBody() throws Exception {
        if (data instanceof byte[]) {
            return ByteBuffer.wrap((byte[]) data);
        }
        ByteBuffer body;
        switch (type) {
            case None: {
                body = ByteBuffer.allocate(0);
                break;
            }
            case Binary: {
                byte[] bytes = (byte[])data;
                body = ByteBuffer.wrap(bytes);
                break;
            }
            case Boolean: {
                body = ByteBuffer.allocate(1);
                body.put(((Boolean)data) ? (byte)1 : (byte)0);
                body.flip();
                break;
            }
            case Integer: {
                if (data instanceof Long) {
                    body = ByteBuffer.allocate(8).putLong((Long)data);
                } else if (data instanceof Integer) {
                    body = ByteBuffer.allocate(4).putInt((Integer) data);
                } else if (data instanceof Short) {
                    body = ByteBuffer.allocate(2).putShort((Short) data);
                } else if (data instanceof Character) {
                    body = ByteBuffer.allocate(1).putChar((Character)data);
                } else {
                    throw new Exception("unknown integer type " + data.getClass());
                }
                body.flip();
                break;
            }
            case UInteger: {
                if (data instanceof Long) {
                    body = ByteBuffer.allocate(8).putLong((Long)data);
                } else if (data instanceof Integer) {
                    body = ByteBuffer.allocate(4).putInt((Integer) data);
                } else if (data instanceof Short) {
                    body = ByteBuffer.allocate(2).putShort((Short) data);
                } else if (data instanceof Byte) {
                    body = ByteBuffer.allocate(1).put((Byte)data);
                } else {
                    throw new Exception("unknown integer type " + data.getClass());
                }
                body.flip();
                break;
            }
            case String: {
                byte[] bytes = ((String)data).getBytes(Charset.forName("UTF-8"));
                body = ByteBuffer.wrap(bytes);
                break;
            }
            case ProtoBuf: {
                byte[] bytes = ((GeneratedMessage)data).toByteArray();
                body = ByteBuffer.wrap(bytes);
                break;
            }
            default: {
                throw new Exception("unknown body type " + type);
            }
        }
        return body;
    }
}
